"""For the search functionality and the associated widgets"""

from PyQt4 import QtGui, QtCore
from functools import partial

FUNCTIONS='all', 'any'

class Filter(object):
    """The callable used to filter a set of elements"""
    def __init__(self, name, logic):
        self.name=name
        self.logic=logic
    def __call__(self, element):
        """Checks if an element satisfies the filter"""
        return self.__testLogicLevel(self.logic, element)
    @classmethod
    def __testLogicLevel(cls, logic, element):
        """Checks through one of the lists of the logic"""
        if not logic: return True #[] is the default for display all
        checks=(cls.__checkSingleTest(test, element) for test in logic[1:])
        if logic[0]=='all':
            return all(checks)
        else:
            return any(checks)
    @classmethod
    def __checkSingleTest(cls, test, element):
        """If a single test, then checks it, otherwise uses the logic level
        test"""
        try:
            att, sub=test.split('=')
            return (sub in element[att] or any(sub in case
                                               for case in element[att]))
        except AttributeError:
            return cls.__testLogicLevel(test, element)

class _FilterGrid(QtGui.QWidget):
    """The displayed logic of the filter, and nothing more"""
    def __init__(self, atts, displayWidget, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.__displayWidget=displayWidget
        self.__filter=_FilterLevel(_WidgetCreator(atts, self))
        self.__layout=QtGui.QGridLayout(self)
        self.displayRefresh()
    def logicChanged(self, _=None):
        self.__displayWidget.filterElements(self.makeFilter())
    def makeFilter(self, name=''):
        return Filter('', self.__filter.getLogic())
    def setLogic(self, logic):
        self.displayClear()
        self.__filter.setLogic(logic)
        self.displayRefresh()
        self.logicChanged()
    def displayClear(self):
        self.__filter.removeFromLayout(self.__layout)
        assert(self.__layout.count()==0)
    def displayRefresh(self):
        _, maxIndent=self.__filter.addIndentLayout(self.__layout)
        self.__filter.addValueLayout(self.__layout, indent=maxIndent)

class _WidgetCreator(object):
    """Responsible for creating the widgets used in the filter grid. It
    connects them to the filter grid on creation, but to nothing else."""
    def __init__(self, atts, parent):
        self.__atts=atts
        self.__parent=parent
    def __connect(self, signal):
        signal.connect(self.__parent.logicChanged)
    def __makeComboBox(self, items):
        comboBox=QtGui.QComboBox(self.__parent)
        for item in items:
            if isinstance(item, list):
                comboBox.addItems(item)
            else:
                comboBox.addItem(item)
        self.__connect(comboBox.currentIndexChanged)
        return comboBox
    makeSelectionCombo=lambda self: self.__makeComboBox(FUNCTIONS)
    makeAttsCombo=lambda self: self.__makeComboBox(self.__atts)
    def __makePushButton(self, text, function):
        button=QtGui.QPushButton(text, self.__parent)
        button.clicked.connect(self.__parent.displayClear)
        button.clicked.connect(function)
        button.clicked.connect(self.__parent.displayRefresh)
        self.__connect(button.clicked)
        return button
    makeAddButton=lambda self, level: self.__makePushButton('+', level.addRow)
    makeDelButton=lambda self, row: self.__makePushButton('x', row.delete)
    makeIndentButton=lambda self, row: self.__makePushButton('->', row.indent)
    makeEqualLabel=lambda self: QtGui.QLabel('=', self.__parent)
    def makeLineEdit(self):
        lineEdit=QtGui.QLineEdit(self.__parent)
        self.__connect(lineEdit.textChanged)
        return lineEdit

class _BaseFilter(QtCore.QObject):
    """The base for both the filter levels and filter rows"""
    sDelete=QtCore.pyqtSignal(QtCore.QObject)
    sIndent=QtCore.pyqtSignal(QtCore.QObject)
    def delete(self):
        self.hideWidgets()
        self.sDelete.emit(self)
    def indent(self):
        self.hideWidgets()
        self.sIndent.emit(self)
        
class _FilterLevel(_BaseFilter):
    """A line of the GUI designed to represent a list of the filter"""
    def __init__(self, widgetCreator, logic=None):
        _BaseFilter.__init__(self)
        if logic is None:
            logic=[FUNCTIONS[0], None]
            #The only case where this is, is the first
        else:
            self.delButton=widgetCreator.makeDelButton(self)
            self.indButton=widgetCreator.makeIndentButton(self)
        self.addButton=widgetCreator.makeAddButton(self)
        self.__widgetCreator=widgetCreator
        self.__combo=widgetCreator.makeSelectionCombo()
        self.setLogic(logic)
    def setLogic(self, logic):
        """Sets the current logic displayed"""
        self.__combo.setCurrentIndex(FUNCTIONS.index(logic[0]))
        self.elements=[]
        for element in logic[1:]:
            self.addElement(element)
    def getLogic(self):
        """Gets the corresponding logic of this level"""
        return [str(self.__combo.currentText())]+[element.getLogic()
                                             for element in self.elements]
    def indentElement(self, element):
        """Removes an element from the filter level"""
        indented=_FilterLevel(self.__widgetCreator, [FUNCTIONS[0],
                                                     element.getLogic()])
        self.elements[self.elements.index(element)]=indented
        try:
            #I have no idea why this is necessary, but it unfortunately is.
            #I will try to figure it out
            element.sDelete.disconnect(self.elements.remove)
            element.sIndent.disconnect(self.indentElement)
        except TypeError:
            pass
        indented.sDelete.connect(self.elements.remove)
        indented.sIndent.connect(self.indentElement)
    def addRow(self):
        """Adds a new row into the grid"""
        return self.addElement(None)
    def addElement(self, element):
        """Creates the GUI element corresponding to the given logic element"""
        if isinstance(element, str) or element is None:
            guiElement=_FilterRow(self.__widgetCreator, element)
        else:
            guiElement=_FilterLevel(self.__widgetCreator, element)
        guiElement.sDelete.connect(self.elements.remove)
        guiElement.sIndent.connect(self.indentElement)
        self.elements.append(guiElement)
        return guiElement
    def addIndentLayout(self, layout, row=0, indent=0):
        try:
            layout.addWidget(self.indButton, row, indent-1)
        except AttributeError:
            pass
        layout.addWidget(self.__combo, row, indent)
        row+=1
        maxIndent=indent
        for element in self.elements:
            row, ind=element.addIndentLayout(layout, row, indent+1)
            maxIndent=max(maxIndent, ind)
        return row, maxIndent
    def addValueLayout(self, layout, row=0, indent=0):
        layout.addWidget(self.addButton, row, indent+4)
        try:
            layout.addWidget(self.delButton, row, indent+5)
        except AttributeError:
            pass
        row+=1
        for element in self.elements:
            row=element.addValueLayout(layout, row, indent)
        return row
    def removeFromLayout(self, layout):
        try:
            layout.removeWidget(self.indButton)
            layout.removeWidget(self.delButton)
        except AttributeError:
            pass
        layout.removeWidget(self.addButton)
        layout.removeWidget(self.__combo)
        for element in self.elements:
            element.removeFromLayout(layout)
    def hideWidgets(self):
        try:
            self.indButton.hide()
            self.delButton.hide()
        except AttributeError:
            pass
        self.addButton.hide()
        self.__combo.hide()
        for element in self.elements:
            element.hideWidgets()
        
class _FilterRow(_BaseFilter):
    def __init__(self, widgetCreator, logic=None):
        _BaseFilter.__init__(self)
        self.__combo=widgetCreator.makeAttsCombo()
        self.__equal=widgetCreator.makeEqualLabel()
        self.__lineEdit=widgetCreator.makeLineEdit()
        self.indButton=widgetCreator.makeIndentButton(self)
        self.delButton=widgetCreator.makeDelButton(self)
        if logic!=None:
            self.setLogic(logic)
    def getLogic(self):
        """Returns the logic represented by this row"""
        return '%s=%s'%(str(self.__combo.currentText()),
                        str(self.__lineEdit.text()))
    def setLogic(self, logic):
        """Sets the logic of this row"""
        combo, lineEdit=logic.split('=')
        for i in xrange(self.__combo.count()):
            if str(self.__combo.itemText(i))==combo:
                self.__combo.setCurrentIndex(i)
                break
        self.__lineEdit.setText(lineEdit)
    def addIndentLayout(self, layout, row, indent):
        layout.addWidget(self.indButton, row, indent-1)
        return row+1, indent
    def addValueLayout(self, layout, row, indent):
        layout.addWidget(self.__combo, row, indent+1)
        layout.addWidget(self.__equal, row, indent+2)
        layout.addWidget(self.__lineEdit, row, indent+3)
        layout.addWidget(self.delButton, row, indent+5)
        return row+1
    def removeFromLayout(self, layout):
        for widget in (self.__combo, self.__equal, self.__lineEdit,
                       self.delButton, self.indButton):
            layout.removeWidget(widget)
    def hideWidgets(self):
        for widget in (self.__combo, self.__equal, self.__lineEdit,
                       self.delButton, self.indButton):
            widget.hide()

class _UpperLine(QtGui.QWidget):
    """The search widget's top line, with the name line and save button"""
    def __init__(self, parent):
        QtGui.QWidget.__init__(self, parent)
        layout=QtGui.QHBoxLayout(self)
        self.__nameEdit=QtGui.QLineEdit('Current Items', self)
        layout.addWidget(self.__nameEdit)
        saveButton=QtGui.QPushButton('Save', self)
        saveButton.clicked.connect(parent.save)
        layout.addWidget(saveButton)
    def __setName(self, value):
        self.__nameEdit.setText(value)
    name=property(lambda self: str(self.__nameEdit.text()), __setName)

class FilterWidget(QtGui.QWidget):
    """The entire widget for the filter"""
    sClicked=QtCore.pyqtSignal()
    def __init__(self, atts, displayWidget, filterList, parent=None):
        QtGui.QWidget.__init__(self, parent)
        layout=QtGui.QVBoxLayout(self)
        self.__upperLine=_UpperLine(self)
        layout.addWidget(self.__upperLine)
        self.__filterGrid=_FilterGrid(atts, displayWidget, self)
        layout.addWidget(self.__filterGrid)
        self.__filterList=filterList
        self.__filterList.sFilterChosen.connect(self.setFilter)
    def save(self):
        """Saves the filter to the list"""
        filter_=self.__filterGrid.makeFilter(self.__upperLine.name)
        self.__filterList.append(filter_)
    def mousePressEvent(self, event):
        self.sClicked.emit()
    def setFilter(self, filter_):
        """Sets this filter to a predefined one"""
        self.__upperLine.name=filter_.name
        self.__filterGrid.setLogic(filter_.logic)

class ExpandingFilterWidget(QtGui.QWidget):
    def __init__(self, atts, displayWidget, filterList, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.__layout=QtGui.QVBoxLayout(self)
        self.__filterWidget=FilterWidget(atts, displayWidget, filterList,
                                         self)
        collapseFunction=lambda : self.setFilterActive(False)
        self.__filterWidget.sClicked.connect(collapseFunction)
        self.__collapsedLine=_CollapsedLine(self)
        self.__layout.addWidget(self.__collapsedLine)
    def setFilterActive(self, activatep):
        """Hides and shows the filter depending on activep. Not safe to use
        if not sure whether the filter is currently shown"""
        if activatep:
            self.__layout.addWidget(self.__filterWidget)
        else:
            self.__layout.removeWidget(self.__filterWidget)

class _CollapsedLine(QtGui.QLabel):
    def __init__(self, parent):
        QtGui.QLabel.__init__(self, 'Search', parent)
        self.setAlignment(QtCore.Qt.AlignHCenter)
        self.__parent=parent
        self.activatep=False
    def mousePressEvent(self, event):
        self.__parent.setFilterActive(self.activatep)
        self.activatep=not self.activatep

class FilterBar(QtGui.QToolBar):
    """The bar holding the known filters"""
    sFilterChosen=QtCore.pyqtSignal(list)
    def __init__(self, filters, parent=None):
        QtGui.QToolBar.__init__(self, parent)
        self.setOrientation(QtCore.Qt.Vertical)
        self.filters=[]
        self.append(Filter('All Songs', [FUNCTIONS[0], None]))
        self.addSeparator()
        for filter_ in filters:
            self.append(Filter(*filter_))
    def append(self, filter_):
        """Adds a filter to the bar"""
        self.filters.append(filter_)
        self.addAction(filter_.name, partial(self.chooseFilter, filter_))
    def chooseFilter(self, filter_):
        """Emits the logic chosen signal with the logic chosen"""
        self.sFilterChosen.emit(filter_)

